class AssignmentError(Exception):
    """A custom class for exceptions raised at the assignment level. This only occurs inside 
    the constructor, which is when the crane is consulted to check if it is able to fulfill 
    the assignment.""" 
    
    
class Assignment(object):
    """Represents the assignment of an activity to a crane."""
    INSUFFICIENT_TIME = "adjusted window size is less than activity's duration"
    Error = AssignmentError
    
    def __init__(self, solution, activity, crane):
        self.solution = solution
        self.activity = activity
        self.crane = crane
        self.id = (self.activity.id, self.crane.id)
        self.window = activity.window.deepcopy()
        # attributes set by set_trajectory() and set_move_period()
        self.x_trajectory = None
        self.y_trajectory = None
        self.traveled_distance = None
        self.move_start = None  # time when crane receives the assignment and starts moving
        self.move_end = None    # time when crane ends movement and starts executing the action
        self.exec_start = None  # same as 'move_end'
        self.exec_end = None    # time when the crane finishes executing the action
        self.duration = None    # time elapsed between receiving the assignment and completing it
        self.penalty = None     # penalty for violating soft end constraints
        # adjust the assignment's window start
        if len(activity.pred) > 0:
            last_pred_completion = max(solution.assignments[p].exec_end for p in activity.pred)
            self.window.start = max(self.window.start, last_pred_completion)
        # check if the window size is at least equal to the activity's duration
        if self.window.size < activity.duration:
            raise Assignment.Error(Assignment.INSUFFICIENT_TIME)
        # check if the crane is capable of fulfilling the assignment
        error = crane._can_fulfill(self)
        if error is not None:
            raise Assignment.Error(error)
            
    def __repr__(self):
        return "Assign(A#%s -> C#%s)" % self.id
        
    def set_move(self, x_trajectory, y_trajectory):
        crane = self.crane
        move_end = (crane.time if len(x_trajectory) == 0 and len(y_trajectory) == 0 else
                    x_trajectory[-1][0] if len(y_trajectory) == 0 else
                    y_trajectory[-1][0] if len(x_trajectory) == 0 else
                    max(x_trajectory[-1][0], y_trajectory[-1][0]))
        self.set_move_period(self.crane.time, move_end)
        # add execution delay to x and y trajectories
        x_trajectory.append((self.exec_end, self.activity.x))
        y_trajectory.append((self.exec_end, self.activity.y))
        # compute traveled distance
        traveled = 0.0
        for movable, trajectory in ((crane.x, x_trajectory), (crane.y, y_trajectory)):
            prev_pos = movable.position
            for time, pos in trajectory:
                traveled += abs(pos - prev_pos)
                prev_pos = pos
        # record trajectories and traveled distance
        self.x_trajectory = x_trajectory
        self.y_trajectory = y_trajectory
        self.traveled_distance = traveled
        
    def set_move_period(self, move_start, move_end):
        self.move_start = move_start
        self.move_end = move_end
        self.exec_start = move_end
        self.exec_end = move_end + self.activity.duration
        self.duration = self.exec_end - move_start
        self.penalty = self.window.penalty(self.exec_end)
        
